<!--
@license
Copyright 2016 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../iron-icons/iron-icons.html">
<link rel="import" href="../paper-button/paper-button.html">
<link rel="import" href="../paper-checkbox/paper-checkbox.html">
<link rel="import" href="../paper-dialog/paper-dialog.html">
<link rel="import" href="../paper-header-panel/paper-header-panel.html">
<link rel="import" href="../paper-icon-button/paper-icon-button.html">
<link rel="import" href="../paper-tabs/paper-tabs.html">
<link rel="import" href="../paper-toolbar/paper-toolbar.html">
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../tf-backend/tf-backend.html">
<link rel="import" href="../tf-dashboard-common/tensorboard-color.html">
<link rel="import" href="../tf-globals/tf-globals.html">
<link rel="import" href="../tf-imports/lodash.html">
<link rel="import" href="../tf-paginated-view/tf-paginated-view-store.html">
<link rel="import" href="../tf-storage/tf-storage.html">
<link rel="import" href="registry.html">

<!--
  tf-tensorboard is the frontend entry point for TensorBoard.

  It implements a toolbar (via paper-header-panel and paper-toolbar)
  that allows the user to toggle among various dashboards.
-->
<dom-module id="tf-tensorboard">
  <template>
    <paper-dialog with-backdrop id="settings">
      <h2>Settings</h2>
      <paper-checkbox id="auto-reload-checkbox" checked="{{autoReloadEnabled}}">
        Reload data every <span>[[autoReloadIntervalSecs]]</span>s.
      </paper-checkbox>
      <paper-input
        id="paginationLimitInput"
        label="Pagination limit"
        always-float-label
        type="number"
        min="1"
        step="1"
        on-change="_paginationLimitChanged"
      ></paper-input>
    </paper-dialog>
    <paper-header-panel>
      <paper-toolbar id="toolbar" slot="header">
        <div id="toolbar-content" slot="top">
          <div class="toolbar-title">[[brand]]</div>
          <template is="dom-if" if="[[_activeDashboardsNotLoaded]]">
            <span class="toolbar-message">
              Loading active dashboards&hellip;
            </span>
          </template>
          <template is="dom-if" if="[[_activeDashboardsLoaded]]">
            <paper-tabs noink
                        scrollable
                        selected="{{_selectedDashboard}}"
                        attr-for-selected="data-dashboard">
              <template
                is="dom-repeat"
                items="[[_dashboardData]]"
                as="dashboardDatum">
                <template
                  is="dom-if"
                  if="[[_isDashboardActive(disabledDashboards, _activeDashboards, dashboardDatum)]]"
                >
                  <paper-tab data-dashboard$="[[dashboardDatum.plugin]]"
                             title="[[dashboardDatum.tabName]]">
                    [[dashboardDatum.tabName]]
                  </paper-tab>
                </template>
              </template>
            </paper-tabs>
            <template
              is="dom-if"
              if="[[_inactiveDashboardsExist(_dashboardData, disabledDashboards, _activeDashboards)]]"
            >
              <paper-dropdown-menu
                label="Inactive"
                no-label-float
                noink
                style="margin-left: 12px"
              >
                <paper-menu
                  id="inactive-dashboards-menu"
                  class="dropdown-content"
                  selected="{{_selectedDashboard}}"
                  attr-for-selected="data-dashboard"
                >
                  <template is="dom-repeat"
                            items="[[_dashboardData]]"
                            as="dashboardDatum">
                    <template
                      is="dom-if"
                      if="[[_isDashboardInactive(disabledDashboards, _activeDashboards, dashboardDatum)]]"
                      restamp
                    >
                      <paper-item
                        data-dashboard$="[[dashboardDatum.plugin]]"
                      >[[dashboardDatum.tabName]]</paper-item>
                    </template>
                  </template>
                </paper-menu>
              </paper-dropdown-menu>
            </template>
          </template>
          <div class="global-actions">
            <paper-icon-button
              icon="refresh"
              on-tap="reload"
              disabled$="[[_isReloadDisabled]]"
              id="reload-button"
            ></paper-icon-button>
            <paper-icon-button
              icon="settings"
              on-tap="openSettings"
              id="settings-button"
            ></paper-icon-button>
            <a href="https://github.com/tensorflow/tensorboard/blob/master/README.md" tabindex="-1">
              <paper-icon-button icon="help-outline"></paper-icon-button>
            </a>
          </div>
        </div>
      </paper-toolbar>

      <div id="content" class="fit">
        <slot id="injected-overview"></slot>
        <template is="dom-if" if="[[_activeDashboardsFailedToLoad]]">
          <div class="warning-message">
            <h3>Failed to load the set of active dashboards.</h3>
            <p>
            This can occur if the TensorBoard backend is no longer
            running. Perhaps this page is cached?
            <p>
            If you think that you’ve fixed the problem, click the reload
            button in the top-right.
            <template is="dom-if" if="[[autoReloadEnabled]]">
            We’ll try to reload every [[autoReloadIntervalSecs]]&nbsp;seconds as well.
            </template>
            <p><i>Last reload: [[_lastReloadTime]]</i>
            <template is="dom-if" if="[[_dataLocation]]">
              <p><i>Log directory: <span id="data_location">[[_dataLocation]]</span></i></p>
            </template>
          </div>
        </template>
        <template is="dom-if" if="[[_showNoDashboardsMessage]]">
          <div class="warning-message">
            <h3>No dashboards are active for the current data set.</h3>
            <p>Probable causes:</p>
            <ul>
              <li>You haven’t written any data to your event files.
              <li>TensorBoard can’t find your event files.
            </ul>
            If you’re new to using TensorBoard, and want to find out how
            to add data and set up your event files, check out the
            <a href="https://github.com/tensorflow/tensorboard/blob/master/README.md">README</a>
            and perhaps the <a href="https://www.tensorflow.org/get_started/summaries_and_tensorboard">TensorBoard tutorial</a>.
            <p>
            If you think TensorBoard is configured properly, please see
            <a href="https://github.com/tensorflow/tensorboard/blob/master/README.md#my-tensorboard-isnt-showing-any-data-whats-wrong">the section of the README devoted to missing data problems</a>
            and consider filing an issue on GitHub.
            <p><i>Last reload: [[_lastReloadTime]]</i>
            <template is="dom-if" if="[[_dataLocation]]">
              <p><i>Data location: <span id="data_location">[[_dataLocation]]</span></i></p>
            </template>
          </div>
        </template>
        <template is="dom-if" if="[[_showNoSuchDashboardMessage]]">
          <div class="warning-message">
            <h3>There’s no dashboard by the name of “<tt>[[_selectedDashboard]]</tt>.”</h3>
            <template is="dom-if" if="[[_activeDashboardsLoaded]]">
              <p>You can select a dashboard from the list above.
            </template>
            <p><i>Last reload: [[_lastReloadTime]]</i>
            <template is="dom-if" if="[[_dataLocation]]">
              <p><i>Data location: <span id="data_location">[[_dataLocation]]</span></i></p>
            </template>
          </div>
        </template>
        <template
          is="dom-repeat"
          id="dashboards-template"
          items="[[_dashboardData]]"
          as="dashboardDatum"
        >
          <div
            class="dashboard-container"
            data-dashboard$="[[dashboardDatum.plugin]]"
            data-selected$="[[_selectedStatus(_selectedDashboard, dashboardDatum.plugin)]]"
          ><!-- Dashboards will be injected here dynamically. --></div>
        </template>
      </div>
    </paper-header-panel>

    <style>
      :host {
        height: 100%;
        display: block;
        background-color: var(--paper-grey-100);
      }

      #toolbar {
        background-color: var(--tb-toolbar-background-color, var(--tb-orange-strong));
        -webkit-font-smoothing: antialiased;
      }

      .toolbar-title {
        font-size: 20px;
        margin-left: 10px;
        text-rendering: optimizeLegibility;
        letter-spacing: -0.025em;
        font-weight: 500;
        display: var(--tb-toolbar-title-display, block);
      }

      .toolbar-message {
        opacity: 0.7;
        -webkit-font-smoothing: antialiased;
        font-size: 14px;
        font-weight: 500;
      }

      paper-tabs {
        flex-grow: 1;
        width: 100%;
        height: 100%;
        --paper-tabs-selection-bar-color: white;
        --paper-tabs-content: {
          -webkit-font-smoothing: antialiased;
          text-transform: uppercase;
        }
      }

      paper-dropdown-menu {
        --paper-input-container-color: rgba(255, 255, 255, 0.8);
        --paper-input-container-focus-color: white;
        --paper-input-container-input-color: white;
        --paper-dropdown-menu-icon: {
          color: white;
        }
        --paper-input-container-input: {
          -webkit-font-smoothing: antialiased;
          font-size: 14px;
          font-weight: 500;
          text-transform: uppercase;
        }
        --paper-input-container-label: {
          -webkit-font-smoothing: antialiased;
          font-size: 14px;
          font-weight: 500;
          text-transform: uppercase;
        }
      }

      #inactive-dashboards-menu {
        --paper-menu-background-color: var(--tb-toolbar-background-color, var(--tb-orange-strong));
        --paper-menu-color: white;
        --paper-menu: {
          text-transform: uppercase;
        }
      }

      .global-actions {
        display: inline-flex; /* Ensure that icons stay aligned */
        justify-content: flex-end;
        text-align: right;
        color: white;
      }

      .global-actions a {
        color: white;
      }

      #toolbar-content {
        width: 100%;
        height: 100%;
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
      }

      #content {
        height: 100%;
      }

      .dashboard-container {
        height: 100%;
      }

      /* Hide unselected dashboards. We still display them within a container
         of height 0 since Plottable produces degenerate charts when charts are
         reloaded while not displayed. */
      .dashboard-container:not([data-selected]) {
        max-height: 0;
        overflow: hidden;
        position: relative;
        /** We further make containers invisible. Some elements may anchor to
            the viewport instead of the container, in which case setting the max
            height here to 0 will not hide them. */
        visibility: hidden;
      }

      .warning-message {
        max-width: 540px;
        margin: 80px auto 0 auto;
      }

      [disabled] {
        opacity: 0.2;
        color: white;
      }
    </style>
  </template>
  <script src="autoReloadBehavior.js"></script>
  <script>
    goog.require('goog.object');

    if (_.isEmpty(tf_tensorboard.dashboardRegistry)) {
      throw new Error('HTML import plugin dashboards *before* tf-tensorboard');
    }

    Polymer({
      is: "tf-tensorboard",
      behaviors: [tf_tensorboard.AutoReloadBehavior],
      properties: {

        /**
         * Title name displayed in top left corner of GUI.
         *
         * This defaults to TensorBoard-X because we recommend against custom
         * builds being branded as TensorBoard.
         */
        brand: {
          type: String,
          value: 'TensorBoard-X',
        },

        /**
         * Deprecated: Equivalent to 'brand' attribute.
         */
        title: {
          type: String,
          observer: '_updateTitle',
        },

        /**
         * We accept a router property only for backward compatibility:
         * setting it triggers an observer that simply calls
         * `tf_backend.setRouter`.
         */
        router: {
          type: Object,
          observer: '_updateRouter',
        },

        /**
         * Deprecated. This used to switch TensorBoard into "demo mode,"
         * loading serialized JSON data from the provided directory.
         */
        demoDir: {
          type: String,
          value: null,
        },

        /**
         * Set this to true to store state in URI hash. Should be true
         * for all non-test purposes.
         */
        useHash: {
          type: Boolean,
          value: false,
        },

        /**
         * A comma-separated list of dashboards not to use.
         */
        disabledDashboards: {
          type: String,
          value: '',
        },

        /**
         * The set of all dashboards. Each object within this array is a
         * DashboardDatum object.
         *
         * @type {Array<Object>}
         */
        _dashboardData: {
          type: Array,
          readOnly: true,
          value: goog.object.getValues(tf_tensorboard.dashboardRegistry),
        },

        /**
         * The set of currently active dashboards. `null` if not yet fetched.
         *
         * TODO(@wchargin): Consider templating in an initial value for
         * this property.
         *
         * @type {Array<string>?}
         */
        _activeDashboards: {
          type: Array,
          value: null,
        },

        /** @type {tf_tensorboard.ActiveDashboardsLoadState} */
        _activeDashboardsLoadState: {
          type: String,
          value: tf_tensorboard.ActiveDashboardsLoadState.NOT_LOADED,
        },
        _activeDashboardsNotLoaded: {
          type: Boolean,
          computed:
            '_computeActiveDashboardsNotLoaded(_activeDashboardsLoadState)',
        },
        _activeDashboardsLoaded: {
          type: Boolean,
          computed:
            '_computeActiveDashboardsLoaded(_activeDashboardsLoadState)',
        },
        _activeDashboardsFailedToLoad: {
          type: Boolean,
          computed:
            '_computeActiveDashboardsFailedToLoad(_activeDashboardsLoadState)',
        },
        _showNoDashboardsMessage: {
          type: Boolean,
          computed:
            '_computeShowNoDashboardsMessage(_activeDashboardsLoaded, _activeDashboards, _selectedDashboard)',
        },
        _showNoSuchDashboardMessage: {
          type: Boolean,
          computed:
            '_computeShowNoSuchDashboardMessage(_dashboardData, _selectedDashboard)',
        },

        /**
         * The plugin name of the currently selected dashboard, or `null` if no
         * dashboard is selected, which corresponds to an empty hash. Defaults
         * to the value stored in the hash.
         */
        _selectedDashboard: {
          type: String,
          value: tf_storage.getString(tf_storage.TAB, /*useLocalStorage=*/false) || null,
          observer: '_selectedDashboardChanged'
        },
        _dashboardToMaybeRemove: String,

        /*
         * Will be set to `true` once our DOM is ready: in particular,
         * once each dashboard has a `<div>` into which we can render its
         * Polymer component root.
         */
        _dashboardContainersStamped: {
          type: Boolean,
          value: false,
        },
        _isReloadDisabled: {
          type: Boolean,
          value: false,
        },
        _lastReloadTime: {
          type: String,
          value: "not yet loaded",
        },
        _dataLocation: {
          type: String,
          value: null,
        },
        _requestManager: {
          type: Object,
          value: () => new tf_backend.RequestManager(),
        },
        _canceller: {
          type: Object,
          value: () => new tf_backend.Canceller(),
        },
      },
      observers: [
        ('_updateSelectedDashboardFromActive(' +
         '_selectedDashboard, _activeDashboards)'),
        ('_ensureSelectedDashboardStamped(' +
         '_dashboardContainersStamped, _activeDashboards, ' +
         '_selectedDashboard)'),
      ],

      _activeDashboardsUpdated(activeDashboards, selectedDashboard) {
      },

      /**
       * @param {string?} disabledDashboards comma-separated
       * @param {Array<string>?} activeDashboards if null, nothing is active
       * @param {Object} dashboardDatum
       * @return {boolean}
       */
      _isDashboardActive(
          disabledDashboards, activeDashboards, dashboardDatum) {
        if ((disabledDashboards || '').split(',').indexOf(
            dashboardDatum.plugin) >= 0) {
          // Explicitly disabled.
          return false;
        }
        if (!(activeDashboards || []).includes(dashboardDatum.plugin)) {
          // Inactive.
          return false;
        }
        return true;
      },

      /**
       * Determine whether a dashboard is enabled but not active.
       *
       * @param {string?} disabledDashboards comma-separated
       * @param {Array<string>?} activeDashboards if null, nothing is active
       * @param {Object} dashboardDatum
       * @return {boolean}
       */
      _isDashboardInactive(
          disabledDashboards, activeDashboards, dashboardDatum) {
        if ((disabledDashboards || '').split(',').indexOf(
            dashboardDatum.plugin) >= 0) {
          // Disabled dashboards don't appear at all; they're not just
          // inactive.
          return false;
        }
        if (!(activeDashboards || []).includes(dashboardDatum.plugin)) {
          // Inactive.
          return true;
        }
        return false;
      },

      _inactiveDashboardsExist(dashboards, disabledDashboards, activeDashboards) {
        if (!activeDashboards) {
          // Not loaded yet. Show nothing.
          return false;
        }
        const workingSet = new Set();
        dashboards.forEach(d => {
          workingSet.add(d.plugin);
        });
        (disabledDashboards || '').split(',').forEach(d => {
          workingSet.delete(d.plugin);
        });
        activeDashboards.forEach(d => {
          workingSet.delete(d.plugin);
        });
        return workingSet.size > 0;
      },

      _getDashboardFromIndex(dashboards, index) {
        return dashboards[index];
      },

      _selectedStatus(selectedDashboard, candidateDashboard) {
        return selectedDashboard === candidateDashboard;
      },

      /**
       * Handle a change in the selected dashboard by persisting the current
       * selection to the hash and logging a pageview if analytics are enabled.
       */
      _selectedDashboardChanged(selectedDashboard) {
        const pluginString = selectedDashboard || '';
        tf_storage.setString(tf_storage.TAB, pluginString, /*useLocalStorage=*/false);
        // Record this dashboard selection as a page view.
        ga('set', 'page', '/' + pluginString);
        ga('send', 'pageview');
      },

      /**
       * If no dashboard is selected but dashboards are available,
       * set the selected dashboard to the first active one.
       */
      _updateSelectedDashboardFromActive(selectedDashboard, activeDashboards) {
        if (activeDashboards && selectedDashboard == null) {
          selectedDashboard = activeDashboards[0] || null;
          if (selectedDashboard != null) {
            // Use location.replace for this call to avoid breaking back-button navigation.
            // Note that this will precede the update to tf_storage triggered by updating
            // _selectedDashboard and make it a no-op.
            tf_storage.setString(tf_storage.TAB, selectedDashboard, /*useLocalStorage=*/false,
                                 /*useLocationReplace=*/true);
            // Note: the following line will re-trigger this handler, but it
            // will be a no-op since selectedDashboard is no longer null.
            this._selectedDashboard = selectedDashboard;
          }
        }
      },

      _updateSelectedDashboardFromHash() {
        const dashboardName = tf_storage.getString(tf_storage.TAB, /*useLocalStorage=*/false);
        this.set('_selectedDashboard', dashboardName || null);
      },

      /**
       * Make sure that the currently selected dashboard actually has a
       * Polymer component; if it doesn't, create one.
       *
       * We have to stamp each dashboard before we can interact with it:
       * for instance, to ask it to reload. Conversely, we can't stamp a
       * dashboard until its _container_ is itself stamped. (Containers
       * are stamped declaratively by a `<dom-repeat>` in the HTML
       * template.)
       *
       * We also wait for the set of active dashboards to be loaded
       * before we stamp anything. This prevents us from stamping a
       * dashboard that's not actually enabled (e.g., if the user
       * navigates to `/#text` when the text plugin is disabled).
       *
       * If the currently selected dashboard is not a real dashboard,
       * this does nothing.
       */
      _ensureSelectedDashboardStamped(containersStamped, activeDashboards,
                                      selectedDashboard) {
        if (!containersStamped || !activeDashboards || !selectedDashboard) {
          return;
        }
        const previous = this._dashboardToMaybeRemove;
        this._dashboardToMaybeRemove = selectedDashboard;
        if (previous && previous != selectedDashboard) {
          if (tf_tensorboard.dashboardRegistry[previous].shouldRemoveDom) {
            const div = this.$$(`.dashboard-container[data-dashboard=${previous}]`);
            if (div.firstChild) {
              div.firstChild.remove();
            }
          }
        }
        const container = this.$$(
            `.dashboard-container[data-dashboard=${selectedDashboard}]`);
        if (!container) {
          // This dashboard doesn't exist. Nothing to do here.
          return;
        }
        const dashboard = tf_tensorboard.dashboardRegistry[selectedDashboard];
        if (container.childNodes.length === 0) {
          const component = document.createElement(dashboard.elementName);
          component.id = 'dashboard';  // used in `_selectedDashboardComponent`
          container.appendChild(component);
        }
        this.set('_isReloadDisabled', dashboard.isReloadDisabled);
      },

      /**
       * Get the Polymer component corresponding to the currently
       * selected dashboard. For instance, the result might be an
       * instance of `<tf-scalar-dashboard>`.
       *
       * If the dashboard does not exist (e.g., the set of active
       * dashboards has not loaded or has failed to load, or the user
       * has selected a dashboard for which we have no implementation),
       * `null` is returned.
       */
      _selectedDashboardComponent() {
        if (!this._dashboardContainersStamped) {
          throw new Error(
            'There is no "selected dashboard" before containers are stamped.');
        }
        const selectedDashboard = this._selectedDashboard;
        var dashboard = this.$$(
          `.dashboard-container[data-dashboard=${selectedDashboard}] #dashboard`);
        return dashboard;
      },

      ready() {
        tf_globals.setUseHash(this.useHash);
        this._updateSelectedDashboardFromHash();
        window.addEventListener('hashchange', () => {
          this._updateSelectedDashboardFromHash();
        }, /*useCapture=*/false);

        // We have to wait for our dashboard-containers to be stamped
        // before we can do anything.
        const dashboardsTemplate = this.$$('#dashboards-template');
        const onDomChange = () => {
          // This will trigger an observer that kicks off everything.
          this._dashboardContainersStamped = true;
        };
        dashboardsTemplate.addEventListener(
          'dom-change', onDomChange, /*useCapture=*/false);

        tf_backend.fetchRuns();
        this._fetchEnvironment();
        this._fetchActiveDashboards();
        this._lastReloadTime = new Date().toString();
      },

      _fetchEnvironment() {
        const url = tf_backend.getRouter().environment();
        return this._requestManager.request(url).then(result => {
          this._dataLocation = result.data_location;
          if (result.window_title) {
            window.document.title = result.window_title;
          }
        });
      },

      _fetchActiveDashboards() {
        this._canceller.cancelAll();
        const updateActiveDashboards = this._canceller.cancellable(result => {
          if (result.cancelled) {
            return;
          }
          const activePlugins = result.value;
          this._activeDashboards = this._dashboardData.map(
              d => d.plugin).filter(p => activePlugins[p]);
          this._activeDashboardsLoadState = tf_tensorboard.ActiveDashboardsLoadState.LOADED;
        });
        const onFailure = () => {
          if (this._activeDashboardsLoadState
              === tf_tensorboard.ActiveDashboardsLoadState.NOT_LOADED) {
            this._activeDashboardsLoadState = tf_tensorboard.ActiveDashboardsLoadState.FAILED;
          } else {
            console.warn(
              "Failed to reload the set of active plugins; using old value.");
          }
        };
        return this._requestManager
          .request(tf_backend.getRouter().pluginsListing())
          .then(updateActiveDashboards, onFailure);
      },

      _computeActiveDashboardsNotLoaded(state) {
        return state === tf_tensorboard.ActiveDashboardsLoadState.NOT_LOADED;
      },
      _computeActiveDashboardsLoaded(state) {
        return state === tf_tensorboard.ActiveDashboardsLoadState.LOADED;
      },
      _computeActiveDashboardsFailedToLoad(state) {
        return state === tf_tensorboard.ActiveDashboardsLoadState.FAILED;
      },
      _computeShowNoDashboardsMessage(loaded, activeDashboards, selectedDashboard) {
        return (loaded
          && activeDashboards.length === 0
          && selectedDashboard == null);
      },
      _computeShowNoSuchDashboardMessage(dashboards, selectedDashboard) {
        return !!selectedDashboard &&
               !dashboards.some(d => d.plugin === selectedDashboard);
      },

      _updateRouter(router) {
        tf_backend.setRouter(router);
      },

      _updateTitle(title) {
        if (title) {
          this.set('brand', title);
        }
      },

      reload() {
        if (this._isReloadDisabled) {
          return;
        }
        this._fetchEnvironment();
        Promise.all([this._fetchActiveDashboards(), tf_backend.fetchRuns()]).then(() => {
          const dashboard = this._selectedDashboardComponent();
          if (dashboard) {
            dashboard.reload();
          }
        });
        this._lastReloadTime = new Date().toString();
      },

      openSettings() {
        this.$.settings.open();
        this.$.paginationLimitInput.value = tf_paginated_view.getLimit();
      },
      _paginationLimitChanged(e) {
        const value = e.target.valueAsNumber;
        // We set type="number" and min="1" on the input, but Polymer
        // doesn't actually enforce those, so we have to check manually.
        if (value === +value && value > 0) {
          tf_paginated_view.setLimit(e.target.valueAsNumber);
        }
      },
    });
  </script>
</dom-module>
